/*
 * Jexer - Java Text User Interface
 *
 * The MIT License (MIT)
 *
 * Copyright (C) 2022 Autumn Lamonte
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 * @author Autumn Lamonte ⚧ Trans Liberation Now
 * @version 1
 */
package jexer.backend;

import java.awt.image.BufferedImage;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import javax.imageio.ImageIO;

/**
 * LegacySixelEncoder turns a BufferedImage into String of sixel image data,
 * using a "uniform" color quantizer.
 */
public class LegacySixelEncoder implements SixelEncoder {

    // ------------------------------------------------------------------------
    // Constants --------------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Palette is used to manage the conversion of images between 24-bit RGB
     * color and a palette of paletteSize colors.
     */
    private class Palette {

        /**
         * Color palette for sixel output, sorted low to high.
         */
        private List<Integer> rgbColors = new ArrayList<Integer>();

        /**
         * Map of color palette index for sixel output, from the order it was
         * generated by makePalette() to rgbColors.
         */
        private int [] rgbSortedIndex = new int[paletteSize];

        /**
         * The color palette, organized by hue, saturation, and luminance.
         * This is used for a fast color match.
         */
        private ArrayList<ArrayList<ArrayList<ColorIdx>>> hslColors;

        /**
         * Number of bits for hue.
         */
        private int hueBits = -1;

        /**
         * Number of bits for saturation.
         */
        private int satBits = -1;

        /**
         * Number of bits for luminance.
         */
        private int lumBits = -1;

        /**
         * Step size for hue bins.
         */
        private int hueStep = -1;

        /**
         * Step size for saturation bins.
         */
        private int satStep = -1;

        /**
         * Cached RGB to HSL result.
         */
        private int hsl[] = new int[3];

        /**
         * The sixel rows of this image.
         */
        private SixelRow [] sixelRows;

        /**
         * ColorIdx records a RGB color and its palette index.
         */
        private class ColorIdx {
            /**
             * The 24-bit RGB color.
             */
            public int color;

            /**
             * The palette index for this color.
             */
            public int index;

            /**
             * Public constructor.
             *
             * @param color the 24-bit RGB color
             * @param index the palette index for this color
             */
            public ColorIdx(final int color, final int index) {
                this.color = color;
                this.index = index;
            }
        }

        /**
         * Metadata regarding one sixel row.
         */
        private class SixelRow {

            /**
             * A set of colors that are present in this row.
             */
            private BitSet colors;

            /**
             * Public constructor.
             */
            public SixelRow() {
                colors = new BitSet(paletteSize);
            }

        }

        /**
         * Public constructor.
         */
        public Palette() {
            makePalette();
        }

        /**
         * Find the nearest match for a color in the palette.
         *
         * @param color the RGB color
         * @return the index in rgbColors that is closest to color
         */
        public int matchColor(final int color) {

            assert (color >= 0);

            /*
             * matchColor() is a critical performance bottleneck.  To make it
             * decent, we do the following:
             *
             *   1. Find the nearest two hues that bracket this color.
             *
             *   2. Find the nearest two saturations that bracket this color.
             *
             *   3. Iterate within these four bands of luminance values,
             *      returning the closest color by Euclidean distance.
             *
             * This strategy reduces the search space by about 97%.
             */
            int red   = (color >>> 16) & 0xFF;
            int green = (color >>>  8) & 0xFF;
            int blue  =  color         & 0xFF;

            if (paletteSize == 2) {
                if (((red * red) + (green * green) + (blue * blue)) < 35568) {
                    // Black
                    return 0;
                }
                // White
                return 1;
            }


            rgbToHsl(red, green, blue, hsl);
            int hue = hsl[0];
            int sat = hsl[1];
            int lum = hsl[2];
            // System.err.printf("%d %d %d\n", hue, sat, lum);

            double diff = Double.MAX_VALUE;
            int idx = -1;

            int hue1 = hue / (360/hueStep);
            int hue2 = hue1 + 1;
            if (hue1 >= hslColors.size() - 1) {
                // Bracket pure red from above.
                hue1 = hslColors.size() - 1;
                hue2 = 0;
            } else if (hue1 == 0) {
                // Bracket pure red from below.
                hue2 = hslColors.size() - 1;
            }

            for (int hI = hue1; hI != -1;) {
                ArrayList<ArrayList<ColorIdx>> sats = hslColors.get(hI);
                if (hI == hue1) {
                    hI = hue2;
                } else if (hI == hue2) {
                    hI = -1;
                }

                int sMin = (sat / satStep) - 1;
                int sMax = sMin + 1;
                if (sMin < 0) {
                    sMin = 0;
                    sMax = 1;
                } else if (sMin == sats.size() - 1) {
                    sMax = sMin;
                    sMin--;
                }
                assert (sMin >= 0);
                assert (sMax - sMin == 1);

                // int sMin = 0;
                // int sMax = sats.size() - 1;

                for (int sI = sMin; sI <= sMax; sI++) {
                    ArrayList<ColorIdx> lums = sats.get(sI);

                    // True 3D colorspace match for the remaining values
                    for (ColorIdx c: lums) {
                        int rgbColor = c.color;
                        int red2   = (rgbColor >>> 16) & 0xFF;
                        int green2 = (rgbColor >>>  8) & 0xFF;
                        int blue2  =  rgbColor         & 0xFF;
                        double newDiff = (red2 - red) * (red2 - red)
                                       + (green2 - green) * (green2 - green)
                                       + (blue2 - blue) * (blue2 - blue);
                        if (newDiff < diff) {
                            idx = rgbSortedIndex[c.index];
                            diff = newDiff;
                        }
                    }
                }
            }

            if (((red * red) + (green * green) + (blue * blue)) < diff) {
                // Black is a closer match.
                idx = 1;
            } else if ((((255 - red) * (255 - red)) +
                    ((255 - green) * (255 - green)) +
                    ((255 - blue) * (255 - blue))) < diff) {

                // White is a closer match.
                idx = paletteSize - 1;
            }
            assert (idx != -1);
            return idx;
        }

        /**
         * Clamp an int value to [0, 255].
         *
         * @param x the int value
         * @return an int between 0 and 255.
         */
        private int clamp(final int x) {
            return Math.max(0, Math.min(x, 255));
        }

        /**
         * Dither an image to a paletteSize palette.  The dithered
         * image cells will contain indexes into the palette.
         *
         * @param image the image to dither
         * @return the dithered image rgb data.  Every pixel is an index into
         * the palette.
         */
        public int [] ditherImage(final BufferedImage image) {

            sixelRows = new SixelRow[(image.getHeight() / 6) + 1];
            for (int i = 0; i < sixelRows.length; i++) {
                sixelRows[i] = new SixelRow();
            }

            int [] rgbArray = image.getRGB(0, 0, image.getWidth(),
                image.getHeight(), null, 0, image.getWidth());

            int height = image.getHeight();
            int width = image.getWidth();
            SixelRow sixelRow;
            for (int imageY = 0; imageY < height; imageY++) {
                sixelRow = sixelRows[imageY / 6];
                for (int imageX = 0; imageX < width; imageX++) {
                    int oldPixel = rgbArray[imageX + (width * imageY)]
                        & 0xFFFFFF;
                    int colorIdx = matchColor(oldPixel);
                    assert (colorIdx >= 0);
                    assert (colorIdx < paletteSize);
                    int newPixel = rgbColors.get(colorIdx);
                    rgbArray[imageX + (width * imageY)] = colorIdx;
                    sixelRow.colors.set(colorIdx);

                    int oldRed   = (oldPixel >>> 16) & 0xFF;
                    int oldGreen = (oldPixel >>>  8) & 0xFF;
                    int oldBlue  =  oldPixel         & 0xFF;

                    int newRed   = (newPixel >>> 16) & 0xFF;
                    int newGreen = (newPixel >>>  8) & 0xFF;
                    int newBlue  =  newPixel         & 0xFF;

                    int redError   = (oldRed - newRed) / 16;
                    int greenError = (oldGreen - newGreen) / 16;
                    int blueError  = (oldBlue - newBlue) / 16;

                    int red, green, blue;
                    if (imageX < image.getWidth() - 1) {
                        int pXpY = rgbArray[imageX + 1 + (width * imageY)];
                        red   = ((pXpY >>> 16) & 0xFF) + (7 * redError);
                        green = ((pXpY >>>  8) & 0xFF) + (7 * greenError);
                        blue  = ( pXpY         & 0xFF) + (7 * blueError);
                        red = clamp(red);
                        green = clamp(green);
                        blue = clamp(blue);
                        pXpY = ((red & 0xFF) << 16)
                             | ((green & 0xFF) << 8) | (blue & 0xFF);
                        rgbArray[imageX + 1 + (width * imageY)] = pXpY;
                        if (imageY < image.getHeight() - 1) {
                            int pXpYp = rgbArray[imageX + 1 + (width * (imageY + 1))];
                            red   = ((pXpYp >>> 16) & 0xFF) + redError;
                            green = ((pXpYp >>>  8) & 0xFF) + greenError;
                            blue  = ( pXpYp         & 0xFF) + blueError;
                            red = clamp(red);
                            green = clamp(green);
                            blue = clamp(blue);
                            pXpYp = ((red & 0xFF) << 16)
                                  | ((green & 0xFF) << 8) | (blue & 0xFF);
                            rgbArray[imageX + 1 + (width * (imageY + 1))] = pXpYp;
                        }
                    } else if (imageY < image.getHeight() - 1) {
                        int pXmYp = rgbArray[imageX - 1 + (width * (imageY + 1))];
                        int pXYp = rgbArray[imageX + (width * (imageY + 1))];

                        red   = ((pXmYp >>> 16) & 0xFF) + (3 * redError);
                        green = ((pXmYp >>>  8) & 0xFF) + (3 * greenError);
                        blue  = ( pXmYp         & 0xFF) + (3 * blueError);
                        red = clamp(red);
                        green = clamp(green);
                        blue = clamp(blue);
                        pXmYp = ((red & 0xFF) << 16)
                              | ((green & 0xFF) << 8) | (blue & 0xFF);
                        rgbArray[imageX - 1 + (width * (imageY + 1))] = pXmYp;

                        red   = ((pXYp >>> 16) & 0xFF) + (5 * redError);
                        green = ((pXYp >>>  8) & 0xFF) + (5 * greenError);
                        blue  = ( pXYp         & 0xFF) + (5 * blueError);
                        red = clamp(red);
                        green = clamp(green);
                        blue = clamp(blue);
                        pXYp = ((red & 0xFF) << 16)
                             | ((green & 0xFF) << 8) | (blue & 0xFF);
                        rgbArray[imageX + (width * (imageY + 1))] = pXYp;
                    }
                } // for (int imageY = 0; imageY < height; imageY++)
            } // for (int imageX = 0; imageX < width; imageX++)

            return rgbArray;
        }

        /**
         * Convert an RGB color to HSL.
         *
         * @param red red color, between 0 and 255
         * @param green green color, between 0 and 255
         * @param blue blue color, between 0 and 255
         * @param hsl the hsl color as [hue, saturation, luminance]
         */
        private void rgbToHsl(final int red, final int green,
            final int blue, final int [] hsl) {

            assert ((red >= 0) && (red <= 255));
            assert ((green >= 0) && (green <= 255));
            assert ((blue >= 0) && (blue <= 255));

            double R = red / 255.0;
            double G = green / 255.0;
            double B = blue / 255.0;
            boolean Rmax = false;
            boolean Gmax = false;
            boolean Bmax = false;
            double min = (R < G ? R : G);
            min = (min < B ? min : B);
            double max = 0;
            if ((R >= G) && (R >= B)) {
                max = R;
                Rmax = true;
            } else if ((G >= R) && (G >= B)) {
                max = G;
                Gmax = true;
            } else if ((B >= G) && (B >= R)) {
                max = B;
                Bmax = true;
            }

            double L = (min + max) / 2.0;
            double H = 0.0;
            double S = 0.0;
            if (min != max) {
                if (L < 0.5) {
                    S = (max - min) / (max + min);
                } else {
                    S = (max - min) / (2.0 - max - min);
                }
            }
            if (Rmax) {
                assert (Gmax == false);
                assert (Bmax == false);
                H = (G - B) / (max - min);
            } else if (Gmax) {
                assert (Rmax == false);
                assert (Bmax == false);
                H = 2.0 + (B - R) / (max - min);
            } else if (Bmax) {
                assert (Rmax == false);
                assert (Gmax == false);
                H = 4.0 + (R - G) / (max - min);
            }
            if (H < 0.0) {
                H += 6.0;
            }
            hsl[0] = (int) (H * 60.0);
            hsl[1] = (int) (S * 100.0);
            hsl[2] = (int) (L * 100.0);

            assert ((hsl[0] >= 0) && (hsl[0] <= 360));
            assert ((hsl[1] >= 0) && (hsl[1] <= 100));
            assert ((hsl[2] >= 0) && (hsl[2] <= 100));
        }

        /**
         * Convert a HSL color to RGB.
         *
         * @param hue hue, between 0 and 359
         * @param sat saturation, between 0 and 100
         * @param lum luminance, between 0 and 100
         * @return the rgb color as 0x00RRGGBB
         */
        private int hslToRgb(final int hue, final int sat, final int lum) {
            assert ((hue >= 0) && (hue <= 360));
            assert ((sat >= 0) && (sat <= 100));
            assert ((lum >= 0) && (lum <= 100));

            double S = sat / 100.0;
            double L = lum / 100.0;
            double C = (1.0 - Math.abs((2.0 * L) - 1.0)) * S;
            double Hp = hue / 60.0;
            double X = C * (1.0 - Math.abs((Hp % 2) - 1.0));
            double Rp = 0.0;
            double Gp = 0.0;
            double Bp = 0.0;
            if (Hp <= 1.0) {
                Rp = C;
                Gp = X;
            } else if (Hp <= 2.0) {
                Rp = X;
                Gp = C;
            } else if (Hp <= 3.0) {
                Gp = C;
                Bp = X;
            } else if (Hp <= 4.0) {
                Gp = X;
                Bp = C;
            } else if (Hp <= 5.0) {
                Rp = X;
                Bp = C;
            } else if (Hp <= 6.0) {
                Rp = C;
                Bp = X;
            }
            double m = L - (C / 2.0);
            int red   = ((int) ((Rp + m) * 255.0)) << 16;
            int green = ((int) ((Gp + m) * 255.0)) << 8;
            int blue  =  (int) ((Bp + m) * 255.0);

            return (red | green | blue);
        }

        /**
         * Create the sixel palette.
         */
        private void makePalette() {
            // Generate the sixel palette.  Because we have no idea at this
            // layer which image(s) will be shown, we have to use a common
            // palette with paletteSize colors for everything, and
            // map the BufferedImage colors to their nearest neighbor in RGB
            // space.

            if (paletteSize == 2) {
                rgbColors.add(0);
                rgbColors.add(0xFFFFFF);
                rgbSortedIndex[0] = 0;
                rgbSortedIndex[1] = 1;
                return;
            }

            // We build a palette using the Hue-Saturation-Luminence model,
            // with 5+ bits for Hue, 2+ bits for Saturation, and 1+ bit for
            // Luminance.  We convert these colors to 24-bit RGB, sort them
            // ascending, and steal the first two indexes for pure black and
            // the last for pure white.  The 8-bit final palette favors
            // bright colors, somewhere between pastel and classic television
            // technicolor.  9- and 10-bit palettes are more uniform.

            // Default at 256 colors.
            hueBits = 5;
            satBits = 2;
            lumBits = 1;

            assert (paletteSize >= 256);
            assert ((paletteSize == 256)
                || (paletteSize == 512)
                || (paletteSize == 1024)
                || (paletteSize == 2048));

            switch (paletteSize) {
            case 512:
                hueBits = 5;
                satBits = 2;
                lumBits = 2;
                break;
            case 1024:
                hueBits = 5;
                satBits = 2;
                lumBits = 3;
                break;
            case 2048:
                hueBits = 5;
                satBits = 3;
                lumBits = 3;
                break;
            }
            hueStep = (int) (Math.pow(2, hueBits));
            satStep = (int) (100 / Math.pow(2, satBits));
            // 1 bit for luminance: 40 and 70.
            int lumBegin = 40;
            int lumStep = 30;
            switch (lumBits) {
            case 2:
                // 2 bits: 20, 40, 60, 80
                lumBegin = 20;
                lumStep = 20;
                break;
            case 3:
                // 3 bits: 8, 20, 32, 44, 56, 68, 80, 92
                lumBegin = 8;
                lumStep = 12;
                break;
            }

            // System.err.printf("<html><body>\n");
            // Hue is evenly spaced around the wheel.
            hslColors = new ArrayList<ArrayList<ArrayList<ColorIdx>>>();

            final boolean DEBUG = false;
            ArrayList<Integer> rawRgbList = new ArrayList<Integer>();

            for (int hue = 0; hue < (360 - (360 % hueStep));
                 hue += (360/hueStep)) {

                ArrayList<ArrayList<ColorIdx>> satList = null;
                satList = new ArrayList<ArrayList<ColorIdx>>();
                hslColors.add(satList);

                // Saturation is linearly spaced between pastel and pure.
                for (int sat = satStep; sat <= 100; sat += satStep) {

                    ArrayList<ColorIdx> lumList = new ArrayList<ColorIdx>();
                    satList.add(lumList);

                    // Luminance brackets the pure color, but leaning toward
                    // lighter.
                    for (int lum = lumBegin; lum < 100; lum += lumStep) {
                        /*
                        System.err.printf("<font style = \"color:");
                        System.err.printf("hsl(%d, %d%%, %d%%)",
                            hue, sat, lum);
                        System.err.printf(";\">=</font>\n");
                        */
                        int rgbColor = hslToRgb(hue, sat, lum);
                        rgbColors.add(rgbColor);
                        ColorIdx colorIdx = new ColorIdx(rgbColor,
                            rgbColors.size() - 1);
                        lumList.add(colorIdx);

                        rawRgbList.add(rgbColor);
                        if (DEBUG) {
                            int red   = (rgbColor >>> 16) & 0xFF;
                            int green = (rgbColor >>>  8) & 0xFF;
                            int blue  =  rgbColor         & 0xFF;
                            int [] backToHsl = new int[3];
                            rgbToHsl(red, green, blue, backToHsl);
                            System.err.printf("%d [%d] %d [%d] %d [%d]\n",
                                hue, backToHsl[0], sat, backToHsl[1],
                                lum, backToHsl[2]);
                        }
                    }
                }
            }
            // System.err.printf("\n</body></html>\n");

            assert (rgbColors.size() == paletteSize);

            /*
             * We need to sort rgbColors, so that toSixel() can know where
             * BLACK and WHITE are in it.  But we also need to be able to
             * find the sorted values using the old unsorted indexes.  So we
             * will sort it, put all the indexes into a HashMap, and then
             * build rgbSortedIndex[].
             */
            Collections.sort(rgbColors);
            HashMap<Integer, Integer> rgbColorIndices = null;
            rgbColorIndices = new HashMap<Integer, Integer>();
            for (int i = 0; i < paletteSize; i++) {
                rgbColorIndices.put(rgbColors.get(i), i);
            }
            for (int i = 0; i < paletteSize; i++) {
                int rawColor = rawRgbList.get(i);
                rgbSortedIndex[i] = rgbColorIndices.get(rawColor);
            }
            if (DEBUG) {
                for (int i = 0; i < paletteSize; i++) {
                    assert (rawRgbList != null);
                    int idx = rgbSortedIndex[i];
                    int rgbColor = rgbColors.get(idx);
                    if ((idx != 0) && (idx != paletteSize - 1)) {
                        /*
                        System.err.printf("%d %06x --> %d %06x\n",
                            i, rawRgbList.get(i), idx, rgbColors.get(idx));
                        */
                        assert (rgbColor == rawRgbList.get(i));
                    }
                }
            }

            // Set the dimmest two colors as true black, and the brightest as
            // true white.  Color 0 should in general never be written to
            // (and won't by this encoder), so we have color 1 for black.
            rgbColors.set(0, 0);
            rgbColors.set(1, 0);
            rgbColors.set(paletteSize - 1, 0xFFFFFF);

            /*
            System.err.printf("<html><body>\n");
            for (Integer rgb: rgbColors) {
                System.err.printf("<font style = \"color:");
                System.err.printf("#%06x", rgb);
                System.err.printf(";\">=</font>\n");
            }
            System.err.printf("\n</body></html>\n");
            */

        }

        /**
         * Emit the sixel palette.
         *
         * @param sb the StringBuilder to append to
         * @param used array of booleans set to true for each color actually
         * used in this cell, or null to emit the entire palette
         * @return the string to emit to an ANSI / ECMA-style terminal
         */
        public String emitPalette(final StringBuilder sb,
            final boolean [] used) {

            // Start the count at 1 so that color 0 remains untouched on the
            // terminal.
            for (int i = 1; i < paletteSize; i++) {
                if ((used == null) || ((used != null) && (used[i] == true))) {
                    int rgbColor = rgbColors.get(i);
                    sb.append(String.format("#%d;2;%d;%d;%d", i,
                            ((rgbColor >>> 16) & 0xFF) * 100 / 255,
                            ((rgbColor >>>  8) & 0xFF) * 100 / 255,
                            ( rgbColor         & 0xFF) * 100 / 255));
                }
            }
            return sb.toString();
        }
    }

    /**
     * Timings records time points in the image generation cycle.
     */
    private class Timings {
        /**
         * Nanotime when the timings were begun.
         */
        public long startTime;

        /**
         * Nanotime after the color map was produced.
         */
        public long buildColorMapTime;

        /**
         * Nanotime after which the RGB image was dithered into an
         * indexed image.
         */
        public long ditherImageTime;

        /**
         * Nanotime after which the dithered image was converted to sixel
         * and emitted.
         */
        public long emitSixelTime;

        /**
         * Nanotime when the timings were finished.
         */
        public long endTime;
    }

    // ------------------------------------------------------------------------
    // Variables --------------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Verbosity level for analysis mode.
     */
    private int verbosity = 0;

    /**
     * If true, use a single shared palette for sixel.
     */
    private boolean sharedPalette = true;

    /**
     * The sixel palette handler.
     */
    private Palette palette = null;

    /**
     * Number of colors in the sixel palette.  Xterm 335 defines the max as
     * 1024.  Valid values are: 2 (black and white), 256, 512, 1024, and
     * 2048.
     */
    private int paletteSize = 1024;

    /**
     * If true, record timings for the image.
     */
    private boolean doTimings = false;

    /**
     * Timings.
     */
    private Timings timings;

    // ------------------------------------------------------------------------
    // Constructors -----------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Public constructor.
     */
    @SuppressWarnings("this-escape")
    public LegacySixelEncoder() {
        reloadOptions();
    }

    // ------------------------------------------------------------------------
    // LegacySixelEncoder -----------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Reload options from System properties.
     */
    public void reloadOptions() {
        // Palette size
        int paletteSize = 1024;
        try {
            paletteSize = Integer.parseInt(System.getProperty(
                "jexer.ECMA48.sixelPaletteSize", "1024"));
            switch (paletteSize) {
            case 2:
            case 256:
            case 512:
            case 1024:
            case 2048:
                this.paletteSize = paletteSize;
                break;
            default:
                // Ignore value
                break;
            }
        } catch (NumberFormatException e) {
            // SQUASH
        }

        // Shared palette
        if (System.getProperty("jexer.ECMA48.sixelSharedPalette",
                "true").equals("false")) {
            sharedPalette = false;
        } else {
            sharedPalette = true;
        }
    }

    /**
     * Create a sixel string representing a bitmap.  The returned string does
     * NOT include the DCS start or ST end sequences.
     *
     * @param bitmap the bitmap data
     * @return the string to emit to an ANSI / ECMA-style terminal
     */
    public String toSixel(final BufferedImage bitmap) {
        StringBuilder sb = new StringBuilder();

        assert (bitmap != null);

        int fullHeight = bitmap.getHeight();

        if (doTimings) {
            timings = new Timings();
            timings.startTime = System.nanoTime();
        }

        if (verbosity >= 1) {
            System.err.printf("toSixel() image is %dx%d, bpp %d\n",
                bitmap.getWidth(), bitmap.getHeight(),
                bitmap.getColorModel().getPixelSize());
        }

        // Dither the image.  It is ok to lose the original here.
        if (palette == null) {
            palette = new Palette();
            if (sharedPalette == true) {
                palette.emitPalette(sb, null);
            }
        }
        if (timings != null) {
            timings.buildColorMapTime = System.nanoTime();
        }

        int [] rgbArray = palette.ditherImage(bitmap);
        int width = bitmap.getWidth();

        if (timings != null) {
            timings.ditherImageTime = System.nanoTime();
        }

        // Collect the raster information
        int rasterHeight = fullHeight;
        int rasterWidth = width;

        if (sharedPalette == false) {
            // Emit the palette, but only for the colors actually used by
            // these cells.
            boolean [] usedColors = new boolean[paletteSize];
            for (int imageX = 0; imageX < width; imageX++) {
                for (int imageY = 0; imageY < fullHeight; imageY++) {
                    usedColors[rgbArray[imageX + (imageY * width)]] = true;
                }
            }
            palette.emitPalette(sb, usedColors);
        }

        // Render the entire row of cells.
        for (int currentRow = 0; currentRow < fullHeight; currentRow += 6) {
            Palette.SixelRow sixelRow = palette.sixelRows[currentRow / 6];

            // Color 0 should in general never be written to (and won't by
            // this encoder).
            for (int i = 1; i < paletteSize; i++) {
                if (!sixelRow.colors.get(i)) {
                    continue;
                }

                /*
                 * We want to avoid tons of memory access, so for each color:
                 *
                 * 1. Create an array for the full width to collect the sum.
                 *
                 * 2. Go down the full row, adding up on the sums.  You have
                 *    to do this up to six times.
                 *
                 * 3. Go one last time down the array and emit the sums.
                 *
                 * It doesn't look that much more complicated than the naive
                 * sum, but should be faster as many cells are captured on
                 * one memory access.
                 */
                int [] row = new int[width];
                for (int j = 0;
                     (j < 6) && (currentRow + j < fullHeight);
                     j++) {

                    int base = width * (currentRow + j);
                    int value = 1 << j;
                    for (int imageX = 0; imageX < width; imageX++) {
                        // Is there was a way to do this without the if?
                        if (rgbArray[base + imageX] == i) {
                            row[imageX] += value;
                        }
                    }
                }

                // Set to the beginning of scan line for the next set of
                // colored pixels, and select the color.
                sb.append("$#");
                sb.append(Integer.toString(i));

                int oldData = -1;
                int oldDataCount = 0;
                for (int imageX = 0; imageX < width; imageX++) {
                    int data = row[imageX];

                    assert (data >= 0);
                    assert (data < 64);
                    data += 63;

                    if (data == oldData) {
                        oldDataCount++;
                    } else {
                        if (oldDataCount == 1) {
                            sb.append((char) oldData);
                        } else if (oldDataCount > 1) {
                            sb.append("!");
                            sb.append(Integer.toString(oldDataCount));
                            sb.append((char) oldData);
                        }
                        oldDataCount = 1;
                        oldData = data;
                    }

                } // for (int imageX = 0; imageX < width; imageX++)

                // Emit the last sequence.
                if (oldDataCount == 1) {
                    sb.append((char) oldData);
                } else if (oldDataCount > 1) {
                    sb.append("!");
                    sb.append(Integer.toString(oldDataCount));
                    sb.append((char) oldData);
                }

            } // for (int i = 0; i < paletteSize; i++)

            // Advance to the next scan line.
            sb.append("-");

        } // for (int currentRow = 0; currentRow < imageHeight; currentRow += 6)

        // Kill the very last "-", because it is unnecessary.
        sb.deleteCharAt(sb.length() - 1);

        // Add the raster information
        sb.insert(0, String.format("\"1;1;%d;%d", rasterWidth, rasterHeight));

        if (timings != null) {
            timings.emitSixelTime = System.nanoTime();
            timings.endTime = System.nanoTime();
        }
        return sb.toString();
    }

    /**
     * If the palette is shared for the entire terminal, emit it to a
     * StringBuilder.
     *
     * @param sb the StringBuilder to write the shared palette to
     */
    public void emitPalette(final StringBuilder sb) {
        if (palette == null) {
            palette = new Palette();
            if (sharedPalette == true) {
                palette.emitPalette(sb, null);
            }
        }
    }

    /**
     * Get the sixel shared palette option.
     *
     * @return true if all sixel output is using the same palette that is set
     * in one DCS sequence and used in later sequences
     */
    public boolean hasSharedPalette() {
        return sharedPalette;
    }

    /**
     * Set the sixel shared palette option.
     *
     * @param sharedPalette if true, then all sixel output will use the same
     * palette that is set in one DCS sequence and used in later sequences
     */
    public void setSharedPalette(final boolean sharedPalette) {
        this.sharedPalette = sharedPalette;
        palette = null;
    }

    /**
     * Get the number of colors in the sixel palette.
     *
     * @return the palette size
     */
    public int getPaletteSize() {
        return paletteSize;
    }

    /**
     * Set the number of colors in the sixel palette.
     *
     * @param paletteSize the new palette size
     */
    public void setPaletteSize(final int paletteSize) {
        if (this.paletteSize == paletteSize) {
            return;
        }

        switch (paletteSize) {
        case 2:
        case 256:
        case 512:
        case 1024:
        case 2048:
            break;
        default:
            throw new IllegalArgumentException("Unsupported sixel palette " +
                " size: " + paletteSize);
        }

        this.paletteSize = paletteSize;
        palette = null;
    }

    /**
     * Clear the sixel palette.  It will be regenerated on the next image
     * encode.
     */
    public void clearPalette() {
        palette = null;
    }

    /**
     * Convert all filenames to sixel.
     *
     * @param args[] the filenames to read
     */
    public static void main(final String [] args) {
        if (args.length == 0) {
            System.err.println("USAGE: java jexer.backend.LegacySixelEncoder [ -v | -vv | -t | -p ] { file1 [ file2 ... ] }");
            System.exit(-1);
        }

        LegacySixelEncoder encoder = new LegacySixelEncoder();
        int successCount = 0;
        boolean performance = false;
        if (encoder.hasSharedPalette()) {
            System.out.print("\033[?1070l");
        } else {
            System.out.print("\033[?1070h");
        }
        System.out.flush();

        for (int i = 0; i < args.length; i++) {
            if ((i == 0) && args[i].equals("-v")) {
                encoder.verbosity = 1;
                encoder.doTimings = true;
                continue;
            }
            if ((i == 0) && args[i].equals("-vv")) {
                encoder.verbosity = 10;
                encoder.doTimings = true;
                continue;
            }
            if ((i == 0) && args[i].equals("-t")) {
                encoder.doTimings = true;
                continue;
            }
            if ((i == 0) && args[i].equals("-p")) {
                encoder.verbosity = 1;
                encoder.doTimings = true;
                performance = true;
                continue;
            }

            try {
                BufferedImage image = ImageIO.read(new FileInputStream(args[i]));
                int count = 1;
                if (performance) {
                    count = 20;
                }
                for (int j = 0; j < count; j++) {

                    StringBuilder sb = new StringBuilder();
                    sb.append("\033Pq");
                    encoder.emitPalette(sb);
                    sb.append(encoder.toSixel(image));
                    sb.append("\033\\");
                    System.out.print(sb.toString());
                    System.out.flush();

                    if (encoder.doTimings) {
                        Timings timings = encoder.timings;
                        assert (timings != null);
                        double mapTime = (double) (timings.buildColorMapTime - timings.startTime) / 1.0e9;
                        double ditherTime = (double) (timings.ditherImageTime - timings.buildColorMapTime) / 1.0e9;
                        double emitSixelTime = (double) (timings.emitSixelTime - timings.ditherImageTime) / 1.0e9;
                        double totalTime = (double) (timings.endTime - timings.startTime) / 1.0e9;

                        System.err.println("Timings:");
                        System.err.printf(" Act.\tmap %6.4fs\tdither %6.4fs\temit %6.4fs\n",
                            mapTime, ditherTime, emitSixelTime);
                        System.err.printf(" Pct.\tmap %4.2f%%\tdither %4.2f%%\temit %4.2f%%\n",
                            100.0 * mapTime / totalTime,
                            100.0 * ditherTime / totalTime,
                            100.0 * emitSixelTime / totalTime);
                        System.err.printf(" total %6.4fs\n", totalTime);
                    }

                }
            } catch (Exception e) {
                System.err.println("Error reading file:");
                e.printStackTrace();
            }

        }
        System.out.print("\033[?1070h");
        System.out.flush();
        if (successCount == args.length) {
            System.exit(0);
        } else {
            System.exit(successCount);
        }
    }

}
